package hardware;

import java.util.LinkedList;

public class Fetch {

	static int PC = 0;
	static LinkedList<Instruction> instructions = new LinkedList<Instruction>();
	static L1Cache l1Cache;
	static L2Cache l2Cache;
	static L3Cache l3Cache;
	
	/**
	 * Fetches m instructions from the cache or memory 
	 * and placing those instructions in the instructions buffer
	 * @param currentTime
	 * 					current cycle in the simulator
	 * @throws Exception
	 */
	public static void fetch(long currentTime) throws Exception {
		int size = Processor.m;
		Instruction instruction;
		int k = 0;

		// Handle the old instructions.
		for (int i = 0; i < size && (!instructions.isEmpty()); i++) {
			instruction = instructions.removeFirst();
			if (instruction.getFetchEndTime() == currentTime) {
				Processor.instructionBuffer.add(instruction);
				instruction.setDecStartTime(currentTime + 1);
			} else {
				instructions.add(instruction);
			}
		}

		// Handle the new instructions.
		if (instructions.isEmpty()) {
			int endIndex = Processor.m - PC%Processor.m;
			for (; PC < Memory.instMem.length&& k < Processor.m && (Processor.instructionBuffer.size() < Processor.instructionBufferSize); PC++) { 
				if(Cache.level == 3){
				try {
					/**
					 * The instruction with address PC is found in L1, so get the m instructions from L1 cache
					 */
					instruction = copyOf(l1Cache.readFromICache(PC + Memory.memorySize, currentTime));
					instruction.setFetchStartTime(currentTime);
					instruction.setFetchEndTime(currentTime + l1Cache.getCycles());
					instructions.add(instruction);
					//Loop for getting the last m-1 instructions from L1 cache
					for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
						PC++;
						instruction = copyOf(l1Cache.readFromICache(PC + Memory.memorySize, currentTime));
						instruction.setFetchStartTime(currentTime);
						instruction.setFetchEndTime(currentTime + l1Cache.getCycles());
						instructions.add(instruction);
					}
				} catch (Exception e) {
					try{
						/**
						 * The instruction with address PC is found in L2, so get the m instructions from L2 cache
						 */
						if(e.getMessage().equals(L1Cache.NOT_FOUND)){
						instruction = copyOf(l2Cache.readFromICache(PC + Memory.memorySize, currentTime));
						instruction.setFetchStartTime(currentTime);
						instruction.setFetchEndTime(currentTime + l1Cache.getCycles() + l2Cache.getCycles());
						instructions.add(instruction);
						
						//Loop for getting the last m-1 instructions from L2 cache
						for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
							PC++;
							instruction = copyOf(l2Cache.readFromICache(PC + Memory.memorySize, currentTime));
							instruction.setFetchStartTime(currentTime);
							instruction.setFetchEndTime(currentTime + l1Cache.getCycles() + l2Cache.getCycles());
							instructions.add(instruction);
						}
					} else {
						throw e;
					}
					} catch(Exception e1) {
						try {
							/**
							 * The instruction with address PC is found in L2, so get the m instructions from L2 cache
							 */
							if(e1.getMessage().equals(L2Cache.NOT_FOUND)){
							instruction = copyOf(l3Cache.readFromICache(PC + Memory.memorySize, currentTime));
							instruction.setFetchStartTime(currentTime);
							instruction.setFetchEndTime(currentTime + l3Cache.getCycles() + l2Cache.getCycles() + l1Cache.getCycles());
							instructions.add(instruction);
							PC++;

							//Loop for getting the last m-1 instructions from L2 cache
							for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
								instruction = copyOf(l3Cache.readFromICache(PC + Memory.memorySize, currentTime));
								instruction.setFetchStartTime(currentTime);
								instruction.setFetchEndTime(currentTime + l3Cache.getCycles() + l2Cache.getCycles() + l1Cache.getCycles());
								instructions.add(instruction);
								PC++;
							}
							} else {
								throw e1;
							}
						} catch(Exception e2){
							if(e2.getMessage().equals(L3Cache.NOT_FOUND)){
							l3Cache.readMissICache(PC + Memory.memorySize, currentTime);
							instruction = copyOf(Memory.instMem[PC]);
							instruction.setFetchStartTime(currentTime);
							instruction.setFetchEndTime(currentTime+ l1Cache.getCycles() + l3Cache.getCycles() + l2Cache.getCycles() + Memory.getAccessTime());
							instructions.add(instruction);
							
							//Loop for getting the last m-1 instructions from the memory
							for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
								PC++;
								instruction = copyOf(Memory.instMem[PC]);
								instruction.setFetchStartTime(currentTime);
								instruction.setFetchEndTime(currentTime+ l1Cache.getCycles() + l3Cache.getCycles() + l2Cache.getCycles() + Memory.getAccessTime());
								instructions.add(instruction);
							}
							} else {
								throw e2;
							}
						}
					}
				}
				} else if(Cache.level == 2){
					try {
						instruction = copyOf(l1Cache.readFromICache(PC + Memory.memorySize, currentTime));
						instruction.setFetchStartTime(currentTime);
						instruction.setFetchEndTime(currentTime + l1Cache.getCycles());
						instructions.add(instruction);
						
						//Loop for getting the last m-1 instructions from L1 cache
						for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
							PC++;
							instruction = copyOf(l1Cache.readFromICache(PC + Memory.memorySize, currentTime));
							instruction.setFetchStartTime(currentTime);
							instruction.setFetchEndTime(currentTime + l1Cache.getCycles());
							instructions.add(instruction);
						}
					} catch (Exception e) {
						try{
							if(e.getMessage().equals(L1Cache.NOT_FOUND)){
							instruction = copyOf(l2Cache.readFromICache(PC + Memory.memorySize, currentTime));
							instruction.setFetchStartTime(currentTime);
							instruction.setFetchEndTime(currentTime + l1Cache.getCycles() + l2Cache.getCycles());
							instructions.add(instruction);
							
							//Loop for getting the last m-1 instructions from L2 cache
							for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
								PC++;
								instruction = copyOf(l2Cache.readFromICache(PC + Memory.memorySize, currentTime));
								instruction.setFetchStartTime(currentTime);
								instruction.setFetchEndTime(currentTime + l1Cache.getCycles() + l2Cache.getCycles());
								instructions.add(instruction);
							}
							} else {
								throw e;
							}
						} catch(Exception e1) {
							if(e.getMessage().equals(L2Cache.NOT_FOUND)){
								l2Cache.readMissICache(PC + Memory.memorySize, currentTime);
								instruction = copyOf(Memory.instMem[PC]);
								instruction.setFetchStartTime(currentTime);
								instruction.setFetchEndTime(currentTime+ l1Cache.getCycles() + l2Cache.getCycles() + Memory.getAccessTime());
								instructions.add(instruction);
								
								//Loop for getting the last m-1 instructions from the memory
								for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
									PC++;
									instruction = copyOf(Memory.instMem[PC]);
									instruction.setFetchStartTime(currentTime);
									instruction.setFetchEndTime(currentTime+ l1Cache.getCycles() + l2Cache.getCycles() + Memory.getAccessTime());
									instructions.add(instruction);
								}
							} else {
								throw e1;
							}
						}
					}
				} else {
					try {
						instruction = copyOf(l1Cache.readFromICache(PC + Memory.memorySize, currentTime));
						instruction.setFetchStartTime(currentTime);
						instruction.setFetchEndTime(currentTime + l1Cache.getCycles());
						instructions.add(instruction);
						
						//Loop for getting the last m-1 instructions from L1 cache
						for(; k<endIndex-1 && PC < Memory.instMem.length -1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
							PC++;
							instruction = copyOf(l1Cache.readFromICache(PC + Memory.memorySize, currentTime));
							instruction.setFetchStartTime(currentTime);
							instruction.setFetchEndTime(currentTime + l1Cache.getCycles());
							instructions.add(instruction);
						}
					} catch (Exception e) {
						if(e.getMessage().equals(L1Cache.NOT_FOUND)){
						l1Cache.readMissICache(PC + Memory.memorySize, currentTime);
						instruction = copyOf(Memory.instMem[PC]);
						instruction.setFetchStartTime(currentTime);
						instruction.setFetchEndTime(currentTime+ l1Cache.getCycles() + Memory.getAccessTime());
						instructions.add(instruction);
						
						//Loop for getting the last m-1 instructions from the memory
						for(; k<endIndex-1 && PC < Memory.instMem.length - 1 && (Processor.instructionBuffer.size() < Processor.instructionBufferSize);k++){
							PC++;
							instruction = copyOf(Memory.instMem[PC]);
							instruction.setFetchStartTime(currentTime);
							instruction.setFetchEndTime(currentTime+ l1Cache.getCycles() + Memory.getAccessTime());
							instructions.add(instruction);
						}
						} else {
							throw e;
						}
				}
				}
				k++;
			}
		}		
 	}
	
	/**
	 * It clones a certain instruction.
	 * @param ins
	 *           instruction we need to get copy from.
	 * @return copy of the instruction.
	 */
	public static Instruction copyOf(Instruction ins){
		Instruction instruction = new Instruction(ins.getID(), ins.getType(), ins.getRegA(), ins.getRegB(), ins.getRegC(), ins.getImmediate(), ins.getExecutionTime());
		return instruction;
	}

}
